package kRender;

import kGeom.*;
import kMesh.kMesh;
import processing.core.*;

public class DrawUtil {

	// processing colors
	public static final int procRed = -65536;
	public static final int procGreen = -16711936;
	public static final int procBlue = -16776961;
	public static final int procBlack = -16777216;
	public static final int procWhite = -1;

	/*
	 * GEOM DRAWING
	 */

	public static void drawVec(kVec tup, int drawSize, PGraphics3D pG) {
		if (!point3dIsBehindCamera(tup, pG)) {
			if (point3dIsOnScreen(tup, pG)) {
				kVec pScr = point3dToPointOnScreen(tup, pG);
				if (pScr.z >= 0) {
					pG.noStroke();
					// TODO: make element coloring
					pG.fill(tup.colorArr[0], tup.colorArr[1], tup.colorArr[2],
							tup.colorArr[3]);

					pG.pushMatrix();
					pG.camera();
					pG.ellipse(pScr.x, pScr.y, drawSize, drawSize);
					pG.popMatrix();
				}
			}
		}
	}

	public static void drawLine(kLine lin, PGraphics3D pG) {
		if ((!point3dIsBehindCamera(lin.StartPoint(), pG))
				|| (!point3dIsBehindCamera(lin.EndPoint(), pG))) {
			kVec paScr = point3dToPointOnScreen(lin.StartPoint(), pG);
			kVec pbScr = point3dToPointOnScreen(lin.EndPoint(), pG);

			if ((line2dIntersectsView(new kLine(paScr, pbScr), pG))
					|| (point2dIsOnScreen(paScr, pG))
					|| (point2dIsOnScreen(pbScr, pG))) {
				// TODO: make element coloring
				pG.noFill();
				pG.stroke(lin.colorArr[0], lin.colorArr[1], lin.colorArr[2],
						lin.colorArr[3]);

				pG.pushMatrix();
				pG.camera();
				pG.line(paScr.x, paScr.y, pbScr.x, pbScr.y);
				pG.popMatrix();
			}
		}
	}
	
	public static void drawPolyline(kPolyline pline, PGraphics3D pG) {
		for (int n = 0; n < pline.countLns(); n++) {
			kLine ln = pline.getLineSegment(n);
			ln.colorArr = pline.colorArr;
			drawLine(ln, pG);
		}
	}

	public static void drawRange(kRange rng, PGraphics3D pG) {
		kVec[] corners = rng.cornerVecs();
		kLine ln = new kLine();

		ln = new kLine(corners[0], corners[1]);
		ln.colorArr = rng.colorArr;
		drawLine(ln, pG);

		ln = new kLine(corners[1], corners[3]);
		ln.colorArr = rng.colorArr;
		drawLine(ln, pG);

		ln = new kLine(corners[3], corners[2]);
		ln.colorArr = rng.colorArr;
		drawLine(ln, pG);

		ln = new kLine(corners[2], corners[0]);
		ln.colorArr = rng.colorArr;
		drawLine(ln, pG);

		ln = new kLine(corners[4], corners[5]);
		ln.colorArr = rng.colorArr;
		drawLine(ln, pG);

		ln = new kLine(corners[5], corners[7]);
		ln.colorArr = rng.colorArr;
		drawLine(ln, pG);

		ln = new kLine(corners[7], corners[6]);
		ln.colorArr = rng.colorArr;
		drawLine(ln, pG);

		ln = new kLine(corners[6], corners[4]);
		ln.colorArr = rng.colorArr;
		drawLine(ln, pG);

		ln = new kLine(corners[0], corners[4]);
		ln.colorArr = rng.colorArr;
		drawLine(ln, pG);

		ln = new kLine(corners[1], corners[5]);
		ln.colorArr = rng.colorArr;
		drawLine(ln, pG);

		ln = new kLine(corners[2], corners[6]);
		ln.colorArr = rng.colorArr;
		drawLine(ln, pG);

		ln = new kLine(corners[3], corners[7]);
		ln.colorArr = rng.colorArr;
		drawLine(ln, pG);

	}

	public static void drawPointCloud(kPointCloud cld, PGraphics3D pG, int ptDrawSize) {
		kVec[] pArr = cld.getPts();
		for (int n = 0; n < pArr.length; n++) {
			pArr[n].colorArr = cld.colorArr;
			drawVec(pArr[n], kSpace.ptDrawSize, pG);
		}
	}

	public static void drawMesh(kMesh msh, PGraphics3D pG, int drawStyle) {
		// TODO: FINISH WORKING OUT HOW TO DRAW A MESH
		// TODO: pick one of these methods for drawing a mesh, and stick with
		// it.

		switch (drawStyle) {
		case 0:
			drawPointCloud(msh.verts, pG, 4);
			break;
		case 1:
			for (int i = 0; i < msh.edges.count(); i++) {
				kLine ln = new kLine(msh.verts.getAt(msh.edges.getAt(i, 0)),msh.verts.getAt(msh.edges.getAt(i, 1)));
				drawLine(ln, pG);
			}
			break;
		case 2:
			for (int i = 0; i < msh.faces.count(); i++) {
				pG.fill(255);
				pG.beginShape();
				int[] vertsOnFace = msh.getVertsOfFace(i);
				for (int j = 0; j < vertsOnFace.length; j++) {
					pG.vertex(msh.verts.getAt(vertsOnFace[j]).x, msh.verts.getAt(vertsOnFace[j]).y, msh.verts.getAt(vertsOnFace[j]).z);
				}
				pG.endShape();
			}
			break;
		default:
			break;
		}

	}
	
	public static void drawTriangle(kTri tri, PGraphics3D pG, int drawStyle) {
		// TODO: allow for other triangle draw styles
		kLine ln;
		switch (drawStyle) {
		case 0:
			pG.noStroke();
			pG.fill(tri.colorArr[0], tri.colorArr[1], tri.colorArr[2],tri.colorArr[3]);
			pG.beginShape();
			pG.vertex( tri.p0.x, tri.p0.y, tri.p0.z );
			pG.vertex( tri.p1.x, tri.p1.y, tri.p1.z );
			pG.vertex( tri.p2.x, tri.p2.y, tri.p2.z );
			pG.endShape();
			
			// sets outline color to max alpha TODO: this is idiosyncratic
			//pG.noFill();
			//ln =  new kLine(tri.p0,tri.p1); ln.colorArr = tri.colorArr; ln.colorArr[3] = tri.colorArr[3]*2; drawLine(ln, pG);
			//ln =  new kLine(tri.p1,tri.p2); ln.colorArr = tri.colorArr; ln.colorArr[3] = tri.colorArr[3]*2; drawLine(ln, pG);
			//ln =  new kLine(tri.p2,tri.p0); ln.colorArr = tri.colorArr; ln.colorArr[3] = tri.colorArr[3]*2; drawLine(ln, pG);
			break;
		case 1:
			ln =  new kLine(tri.p0,tri.p1); ln.colorArr = tri.colorArr; drawLine(ln, pG);
			ln =  new kLine(tri.p1,tri.p2); ln.colorArr = tri.colorArr; drawLine(ln, pG);
			ln =  new kLine(tri.p2,tri.p0); ln.colorArr = tri.colorArr; drawLine(ln, pG);
			break;
		default:
			break;
		}

	}	

	public static void drawVecField(kVecField vField, PGraphics3D pG, int drawStyle){
		
		for (int u = 0; u < vField.res(); u++) 
			for (int v = 0; v < vField.res(); v++) 
				for (int w = 0; w < vField.res(); w++) {
					kVec vec = vField.getAt(u, v, w);
					if (!vec.isIdentical(new kVec(0,0,0)) && !vec.isIdentical(vField.dontDrawVec)){
						kVec cent = vField.getCellCentroidAt(u,v,w);
						kLine ln = new kLine(cent,vec,vField.vecDrawScale*vec.length());
						ln.colorArr = vField.colorArr;
						cent.colorArr = vField.colorArr;
						drawLine( ln , pG);
						drawVec( cent, 3 , pG );
					}
				}
	}
	
	
	/*
	 * UTILITY METHODS
	 */

	public static kVec cameraPt(PGraphics3D pG) {
		
		return multiply(new kVec(), pG.modelviewInv);
	}

	public static kVec multiply(kVec v, PMatrix3D m) {
		float xx = v.x * m.m00 + v.y * m.m01 + v.z * m.m02 + m.m03;
		float yy = v.x * m.m10 + v.y * m.m11 + v.z * m.m12 + m.m13;
		float zz = v.x * m.m20 + v.y * m.m21 + v.z * m.m22 + m.m23;
		return new kVec(xx, yy, zz);
	}

	public static boolean point3dIsBehindCamera(kVec p, PGraphics3D pG) {
		kVec pt = multiply(p, pG.camera);
		if (pt.z < -kSpace.clip)
			return false;
		return true;
	}

	public static boolean point3dIsOnScreen(kVec p, PGraphics pG) {
		float xScr = pG.screenX(p.x, p.y, p.z);
		float yScr = pG.screenY(p.x, p.y, p.z);
		return point2dIsOnScreen(new kVec(xScr, yScr), pG);
	}

	public static boolean point2dIsOnScreen(kVec p, PGraphics pG) {
		if ((p.x > 0) && (p.y > 0) && (p.x < pG.width) && (p.y < pG.height)) {
			return true;
		}
		return false;
	}

	public static kVec point3dToPointOnScreen(kVec p, PGraphics pG) {
		return new kVec(pG.screenX(p.x, p.y, p.z), pG.screenY(p.x, p.y, p.z), 0);
	}

	/**
	 * Test if the line segment (x1,y1)-&gt;(x2,y2) intersects the line segment
	 * (x3,y3)-&gt;(x4,y4).
	 * 
	 * @param x1
	 *            the first x coordinate of the first segment
	 * @param y1
	 *            the first y coordinate of the first segment
	 * @param x2
	 *            the second x coordinate of the first segment
	 * @param y2
	 *            the second y coordinate of the first segment
	 * @param x3
	 *            the first x coordinate of the second segment
	 * @param y3
	 *            the first y coordinate of the second segment
	 * @param x4
	 *            the second x coordinate of the second segment
	 * @param y4
	 *            the second y coordinate of the second segment
	 * @return true if the segments intersect
	 */
	private static boolean lines2dIntersect(double x1, double y1, double x2,
			double y2, double x3, double y3, double x4, double y4) {
		double a1, a2, a3, a4;

		// deal with special cases
		if ((a1 = area2(x1, y1, x2, y2, x3, y3)) == 0.0) {
			// check if p3 is between p1 and p2 OR
			// p4 is collinear also AND either between p1 and p2 OR at opposite
			// ends
			if (between(x1, y1, x2, y2, x3, y3)) {
				return true;
			} else {
				if (area2(x1, y1, x2, y2, x4, y4) == 0.0) {
					return between(x3, y3, x4, y4, x1, y1)
							|| between(x3, y3, x4, y4, x2, y2);
				} else {
					return false;
				}
			}
		} else if ((a2 = area2(x1, y1, x2, y2, x4, y4)) == 0.0) {
			// check if p4 is between p1 and p2 (we already know p3 is not
			// collinear)
			return between(x1, y1, x2, y2, x4, y4);
		}

		if ((a3 = area2(x3, y3, x4, y4, x1, y1)) == 0.0) {
			// check if p1 is between p3 and p4 OR
			// p2 is collinear also AND either between p1 and p2 OR at opposite
			// ends
			if (between(x3, y3, x4, y4, x1, y1)) {
				return true;
			} else {
				if (area2(x3, y3, x4, y4, x2, y2) == 0.0) {
					return between(x1, y1, x2, y2, x3, y3)
							|| between(x1, y1, x2, y2, x4, y4);
				} else {
					return false;
				}
			}
		} else if ((a4 = area2(x3, y3, x4, y4, x2, y2)) == 0.0) {
			// check if p2 is between p3 and p4 (we already know p1 is not
			// collinear)
			return between(x3, y3, x4, y4, x2, y2);
		} else { // test for regular intersection
			return ((a1 > 0.0) ^ (a2 > 0.0)) && ((a3 > 0.0) ^ (a4 > 0.0));
		}
	}

	private static boolean lines2dIntersect(kLine lnA, kLine lnB) {
		return lines2dIntersect(lnA.StartPoint().x, lnA.StartPoint().y, lnA
				.EndPoint().x, lnA.EndPoint().y, lnB.StartPoint().x, lnB
				.StartPoint().y, lnB.EndPoint().x, lnB.EndPoint().y);
	}

	private static boolean line2dIntersectsView(kLine ln, PGraphics pG) {
		kLine lnTop = new kLine(0, 0, pG.width, 0);
		kLine lnRight = new kLine(pG.width, 0, pG.width, pG.height);
		kLine lnBottom = new kLine(pG.width, pG.height, 0, pG.height);
		kLine lnLeft = new kLine(0, pG.height, 0, 0);

		return (lines2dIntersect(ln, lnTop) || lines2dIntersect(ln, lnRight)
				|| lines2dIntersect(ln, lnBottom) || lines2dIntersect(ln,
				lnLeft));
	}

	/**
	 * Computes twice the (signed) area of the triangle defined by the three
	 * points. This method is used for intersection testing.
	 * 
	 * @param x1
	 *            the x-coordinate of the first point.
	 * @param y1
	 *            the y-coordinate of the first point.
	 * @param x2
	 *            the x-coordinate of the second point.
	 * @param y2
	 *            the y-coordinate of the second point.
	 * @param x3
	 *            the x-coordinate of the third point.
	 * @param y3
	 *            the y-coordinate of the third point.
	 * @return Twice the area.
	 */
	private static double area2(double x1, double y1, double x2, double y2,
			double x3, double y3) {
		return (x2 - x1) * (y3 - y1) - (x3 - x1) * (y2 - y1);
	}

	/**
	 * Returns true if (x3, y3) lies between (x1, y1) and (x2, y2), and false
	 * otherwise, This test assumes that the three points are collinear, and is
	 * used for intersection testing.
	 * 
	 * @param x1
	 *            the x-coordinate of the first point.
	 * @param y1
	 *            the y-coordinate of the first point.
	 * @param x2
	 *            the x-coordinate of the second point.
	 * @param y2
	 *            the y-coordinate of the second point.
	 * @param x3
	 *            the x-coordinate of the third point.
	 * @param y3
	 *            the y-coordinate of the third point.
	 * @return A boolean.
	 */
	private static boolean between(double x1, double y1, double x2, double y2,
			double x3, double y3) {
		if (x1 != x2) {
			return (x1 <= x3 && x3 <= x2) || (x1 >= x3 && x3 >= x2);
		} else {
			return (y1 <= y3 && y3 <= y2) || (y1 >= y3 && y3 >= y2);
		}
	}

}
